/*
 * Matt Adams
 * TCSS 558 HW1
 * Fall 2013
 */
package client;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.util.StringTokenizer;

import tools.Log;

/**
 * Handles TCP client functionality for spelling client-server program.
 * 
 * @author Matt Adams
 */
public class TCPSpellingClient extends SpellingClient implements Runnable {
    
    //Static variables used in TCP client.
    private static final String OK = "OK";
    private static final String NO = "NO";
    private static final String BYE = "GOODBYE";
    private static final String INVALID = "INVALID";
    private static final int SOCKET_TIMEOUT = 60000;
    
    /**
     * Socket used for TCP client connection.
     */
    private Socket socket;
    
    /**
     * Reader used to receive input from a TCP server.
     */
    private BufferedReader bufIn;
    
    /**
     * Writer used to send output to a TCP server.
     */
    private BufferedWriter bufOut;
    
    /**
     * Constructs TCPSpellingClient object.
     * 
     * @param hostPortWordArray
     */
    public TCPSpellingClient(String[] hostPortWordArray) {
        
        super(hostPortWordArray);
        setupSocket();
        try {
            this.bufIn = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
            this.bufOut = new BufferedWriter(new OutputStreamWriter(this.socket.getOutputStream()));
        } catch (IOException e) {
            Log.err("Failed to create buffered reader/writer: " + e);
        }   
    }
    
    /**
     * Starts TCP client.
     */
    public void start() {
        for (int i = 0; i < this.queryWords.size(); i++) {
            Log.out("Querying server for word: " + this.queryWords.get(i));
            queryServer(this.queryWords.get(i));
        }
        processUserInput();
        close();
    }
    
    /**
     * Accepts user query from standard-in and sends the query to a TCP server. The server's
     * response is shown using standard-out.
     */
    private void processUserInput() {
        BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter stdOut = new BufferedWriter(new OutputStreamWriter(System.out));
        String fromServer;      
        try { 
            sendInputFromUser(stdIn);
            while ((fromServer = this.bufIn.readLine()) != null) {
                stdOut.write(fromServer + "\n");
                stdOut.flush();
                if (fromServer.equals(BYE) || fromServer.equals(INVALID)) {
                    break;
                }
                sendInputFromUser(stdIn);
            }
        } catch (IOException e) {
            Log.err("Error receiving input from user: " + e);          
        }
    }
    
    /**
     * Sends user input to a TCP server.
     * 
     * @param stdIn Standard-In reader.
     * @throws IOException IOExceptino from writing data to TCP server.
     */
    private void sendInputFromUser(final BufferedReader stdIn) throws IOException {
        String fromUser = stdIn.readLine();
        if (fromUser != null) {
            this.bufOut.write(fromUser + "\r\n");
            this.bufOut.flush();
        }
    }
    
    /**
     * Sends queried word to TCP Server. Only used when words are listed in the initial
     * command line argument used.
     */
    protected void send(final String word) {

        try {
            bufOut.write("1 " + word + "\r\n");
            bufOut.flush();
        } catch (IOException e) {
            Log.err("Error writing to output stream: " + e);
        }
    }
    
    /**
     * Receives response from server.
     */
    protected void receive(final String word) {
        try {
            final String serverResponse = bufIn.readLine();
            final String nullString = null;
            if (serverResponse.contains(OK)) {
                displayQueryResponse(word, -1, nullString);
            } else if (serverResponse.contains(NO)) {
                StringTokenizer tokenizer = new StringTokenizer(serverResponse);
                final int numberOfTokens = tokenizer.countTokens();
                if (numberOfTokens <= 1) {
                    Log.err("Malformed server response, incorrect number of returned tokens: " + numberOfTokens);
                } else if (numberOfTokens == 2) {
                    displayQueryResponse(word, 0, nullString);
                } else {
                    final StringBuilder sb = new StringBuilder();
                    checkResponseNumber(1, tokenizer.nextToken());
                    tokenizer.nextToken();
                    while (tokenizer.hasMoreTokens()) {
                        sb.append(tokenizer.nextToken());
                        sb.append(" ");
                    }
                    displayQueryResponse(word, numberOfTokens - 2, sb.toString());
                }
            }
        } catch (IOException e) {
            Log.err("Error processing server response: " + e);
        }
        
    }
    
    /**
     * Checks that the number in the query and server response match.
     * 
     * @param number Number in query.
     * @param response Number in server response.
     */
    public void checkResponseNumber(final int number, final String response) {
        try {
            final int responseNumber = Integer.parseInt(response);
            if (number != responseNumber) {
                Log.err("Response number incorrect. Sent: " + number + " Returned: " + responseNumber);
                close();
            }
        } catch (NumberFormatException e) {
            Log.err("A nonvalid integer was returned in server response.");
            close();
        }
    }
    
    /**
     * Closes connection.
     */
    private void close() {
        Log.out("Closing connection.");
        try {
            this.socket.close();
            this.bufIn.close();
            this.bufOut.close();
        } catch (IOException e) {
            Log.err("Failed to close open connections: " + e);
        }
        Log.out("Closed.");
    }
    
    /**
     * Sets up socket.
     */
    private void setupSocket() {      
        try {
            this.socket = new Socket(this.address, this.port);
            this.socket.setSoTimeout(SOCKET_TIMEOUT);
            Log.out("Socket opened on port: " + this.socket.getLocalPort());
        } catch (IOException e) {
            Log.err("Failed to create tcp socket: " + e.getMessage());
            errorClose();
        }
 
    }

    @Override
    public void run() {
        this.start();       
    }
    
    /**
     * Main to run client.
     * @param hostPortWordArray Listing of host, port, and words to be queried.
     */
    public static void main(String[] hostPortWordArray) {    
        //final TCPSpellingClient tcpSpellingClient = new TCPSpellingClient(hostPortWordArray);
        //tcpSpellingClient.start();
        
        //Testing Concurrent TCP Server:
        final TCPSpellingClient tcpClientOne = new TCPSpellingClient(hostPortWordArray);
        final TCPSpellingClient tcpClientTwo = new TCPSpellingClient(new String[] {"Jamada-PC", "4444", "negative", "positive", "neutron", "electron", "neep"});
        final TCPSpellingClient tcpClientThree = new TCPSpellingClient(new String[] {"Jamada-PC", "4444", "atom", "pulse", "supernova", "ffun", "neeerd"});
        final TCPSpellingClient tcpClientFour = new TCPSpellingClient(new String[] {"Jamada-PC", "4444", "four", "quatro"});
        final TCPSpellingClient tcpClientFive = new TCPSpellingClient(new String[] {"Jamada-PC", "4444", "five", "cinco"});
        Thread one = new Thread(tcpClientOne);
        Thread two = new Thread(tcpClientTwo);
        Thread three = new Thread(tcpClientThree);
        Thread four = new Thread(tcpClientFour);
        Thread five = new Thread(tcpClientFive);
        one.start();
        two.start();
        three.start();
        four.start();
        five.start();
    }

}
